feat(story-2.1): Middleware protocol, Next type, and chain composition#8
Merged
Merged
Conversation
Ports the archived Middleware Execution Model decision forward to the superpowers flow. Decisions: runtime-checkable Protocol matching Transport / ResponseDecoder; TypeAlias for Next (3.11 floor rules out PEP 695); private compose() at _internal/chain.py using a recursive closure fold with transport.__call__ as the bottom of the chain; Sequence[Middleware] over list; no exception handling in compose so CancelledError propagates naturally; public exports at both httpware.middleware.* and httpware.* (matches Request/Response). Strict epic boundary — decorators (2-2), Request helpers (2-3), auth coercion (2-4), AsyncClient wiring (2-5), and streaming chain (4-3) land as their own stories. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds src/httpware/middleware/__init__.py defining: - Next: TypeAlias = Callable[[Request], Awaitable[Response]] - Middleware: @runtime_checkable Protocol with async __call__(request, next) Matches Transport and ResponseDecoder shape. The `next` parameter shadows the Python builtin (standard for this pattern); structural typing matches by position, so concrete middleware may rename it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eware cases Adds src/httpware/_internal/chain.compose(middlewares, transport) -> Next using a recursive closure fold. Bottom of chain is transport.__call__ (bound method, no wrapper). Empty sequence returns transport.__call__ directly. Tests verify both cases against a minimal _OkTransport fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three tests verifying the onion-execution order (outer→inner→ transport→inner→outer), request mutation via with_header propagates to the inner middleware, and outer middleware can return a modified Response after awaiting next. No production code changes; the existing compose() implementation handles all three cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…reusability Adds five tests covering the remaining acceptance criteria: - short-circuit middleware bypasses inner layers and the transport - exceptions raised inside middleware bubble through unchanged - exceptions raised by the transport pass through middleware unchanged - asyncio.CancelledError propagates (NFR15) - the Next returned by compose can be reused across sequential requests No production code changes; compose's no-try/except design carries the cancellation guarantee. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Middleware and Next to httpware/__init__.py imports and __all__ so consumers can `from httpware import Middleware, Next` in addition to the subpackage path. Matches the existing Request/Response/Transport re-export pattern. CHANGELOG records the Story 2.1 surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AsyncClient ↔ Middlewareseam (Seam 2):Middlewareruntime-checkable Protocol withasync def __call__(self, request: Request, next: Next) -> Response, andNext = Callable[[Request], Awaitable[Response]]exported at bothhttpware.middleware.*andhttpware.*.compose(middlewares, transport) -> Nextathttpware._internal.chain. Recursive closure fold;transport.__call__is the bottom of the chain; empty list returnstransport.__call__directly. Notry/exceptincomposeor_wrap—asyncio.CancelledErrorand user-raised exceptions propagate untouched (NFR15).isinstance, theNextalias's resolved type, package-root re-export, and reusability of the composedNext.Out of scope (subsequent stories): phase decorators (2-2), Request immutability helpers beyond what already exists (2-3), auth coercion (2-4), AsyncClient wiring (2-5), streaming chain (4-3).
Spec + plan:
docs/superpowers/specs/2026-05-31-middleware-protocol-and-chain-design.md,docs/superpowers/plans/2026-05-31-middleware-protocol-and-chain-plan.md.Test plan
just test— 174 passed, 1 deselected (perf), 100% line coverage on the new modules.just lint-ci—eof-fixer,ruff format --check,ruff check --no-fix,ty checkall clean.tests/test_no_httpx2_leakage.pypasses — no `httpx2` import added.from httpware import Middleware, Nextandfrom httpware.middleware import Middleware, Nextboth resolve.🤖 Generated with Claude Code